﻿using Microsoft.VisualStudio.TestTools.UnitTesting;
using UnitBox.Fixtures;
using World.Testing;

namespace UnitBox {
	[TestClass]
	public class InterceptSpecs : SpecContext {

		#region context setup/teardown

		protected override void ContextSetup() {
			// ensure static fixture test state is reset
			StaticThing.ResetState();

#if DEBUG
			// ensure UnitBox lockdown is reset, so that tests remain isolated from each other. this is a feature created only for UnitBox tests, and is not even compiled in release mode since it would go against the very intent of the feature (to be irreversible)
			UnitBoxInterceptions.ResetLockdown();
#endif
		}


		[TestCleanup]
		public virtual void TestCleanup() {
			// ensure no interceptions affect tests. this keeps tests isoalted
			UnitBoxInterceptions.ResetAllInterceptions();
		}

		#endregion


		#region contexts

		[TestClass]
		public class WhenNoInterceptionHasYetBeenConfigured : InterceptSpecs {

			#region specs
			
			[TestMethod]
			public void WhenInvokingAStaticProperty_ShouldCallTheRealProperty() {
				WhenInvokingAStaticPropertyShouldCallTheRealProperty();
			}


			[TestMethod]
			public void WhenInvokingAStaticMethod_ShouldCallTheRealMethod() {
				WhenInvokingAStaticMethodShouldCallTheRealMethod();
			}

			#endregion

		}


		[TestClass]
		public class WhenConfiguredToInterceptAStaticCallButWithoutSpecifyingAnyReplacementBehaviour : InterceptSpecs {

			#region context setup

			protected override void ContextSetup() {
				base.ContextSetup();

				Intercept.NextCallTo( () => StaticThing.DoSomething() );
				// NOTE: intentionally do not replace the method's behaviour
			}

			#endregion


			#region specs

			[TestMethod]
			public void WhenInvokingAStaticProperty_ShouldCallTheRealProperty() {
				WhenInvokingAStaticPropertyShouldCallTheRealProperty();
			}


			[TestMethod]
			public void WhenInvokingAStaticMethod_ShouldCallTheRealMethod() {
				WhenInvokingAStaticMethodShouldCallTheRealMethod();
			}


			// TODO(specs): what other specs are needed here? ie: calls to all other kinds of static methods

			#endregion

		}


		[TestClass]
		public class WhenConfiguredToInterceptAStaticCallAndReplaceItsBehaviour : InterceptSpecs {

			#region context objects

			protected static bool DidInterceptPropertyGetter { get; set; }
			protected static bool DidInterceptPropertySetter { get; set; }
			protected static bool DidInterceptMethodWithNoReturnValue { get; set; }
			protected static bool DidInterceptMethodWithReturnValue { get; set; }

			#endregion


			#region context setup

			protected override void ContextSetup() {
				base.ContextSetup();
				// reset static test state
				DidInterceptMethodWithNoReturnValue = false;
				DidInterceptMethodWithReturnValue = false;

				//InterceptTheNextCallToTheStaticMethods();
			}

			#endregion


			#region specs

			[TestMethod]
			public virtual void WhenMakingTheNextPropertyCall_ShouldInterceptThePropertyCall() {
				InterceptTheNextCallToTheStaticProperties();

				Call.Static( () => StaticThing.SomeThing );
				const string realValue = "It's the real deal yo.";
				Call.StaticSetter( () => StaticThing.SomeThing, realValue );
				EnsurePropertyGetterWasNotCalled();
				EnsurePropertySetterWasNotCalled();
				DidInterceptPropertyGetter.ShouldBe( true );
				// TODO(revive)
//				DidInterceptPropertySetter.ShouldBe( true );
			}


			[TestMethod]
			public virtual void WhenMakingTheNextMethodCall_ShouldInterceptTheMethodCall() {
				InterceptTheNextCallToTheStaticMethods();

				Call.Static( () => StaticThing.DoSomething() );
				Call.Static( () => StaticThing.DoSomethingThatReturns() );
				EnsureDoSomethingMethodWasNotCalled();
				EnsureDoSomethingThatReturnsMethodWasNotCalled();
				DidInterceptMethodWithNoReturnValue.ShouldBe( true );
				DidInterceptMethodWithReturnValue.ShouldBe( true );
			}


			// TODO(requirements): this needs revisiting, as properties should likely act like properties, which is to say they keep returning the value that they were set up with until otherwise configured
			// NOTE: this could be achieved once behaviour is introduced to return the same value forever (future release)
			// NOTE: for now, the properties act the same as methods, but this is slightly awkward & wonky (ie: unintuitive)
			[TestMethod]
			public virtual void WhenConfiguringASubsequentInterceptionToTheSameProperty_ShouldInterceptCallsInTheSameOrderTheyWereConfigured() {
				InterceptTheNextCallToTheStaticProperties();
				InterceptTheNextCallToTheStaticProperties();

				// call the static properties twice, ensuring the properties were intercepted both times
				Call.Static( () => StaticThing.SomeThing );
				const string realValue1 = "It's the real deal yo.";
				Call.StaticSetter( () => StaticThing.SomeThing, realValue1 );
				EnsurePropertyGetterWasNotCalled();
				EnsurePropertySetterWasNotCalled();
				DidInterceptPropertyGetter.ShouldBe( true );
				// TODO(revive)
				//				DidInterceptPropertySetter.ShouldBe( true );

				StaticThing.ResetState();
				DidInterceptPropertyGetter = false;
				DidInterceptPropertySetter = false;

				Call.Static( () => StaticThing.SomeThing );
				const string realValue2 = "It's the real deal yo.";
				Call.StaticSetter( () => StaticThing.SomeThing, realValue2 );
				EnsurePropertyGetterWasNotCalled();
				EnsurePropertySetterWasNotCalled();
				DidInterceptPropertyGetter.ShouldBe( true );
				// TODO(revive)
				//				DidInterceptPropertySetter.ShouldBe( true );
			}


			[TestMethod]
			public virtual void WhenConfiguringASubsequentInterceptionToTheSameMethod_ShouldInterceptCallsInTheSameOrderTheyWereConfigured() {
				InterceptTheNextCallToTheStaticMethods();
				InterceptTheNextCallToTheStaticMethods();

				// call the static methods twice, ensuring the methods were intercepted both times
				Call.Static( () => StaticThing.DoSomething() );
				Call.Static( () => StaticThing.DoSomethingThatReturns() );
				EnsureDoSomethingMethodWasNotCalled();
				EnsureDoSomethingThatReturnsMethodWasNotCalled();
				DidInterceptMethodWithNoReturnValue.ShouldBe( true );
				DidInterceptMethodWithReturnValue.ShouldBe( true );

				StaticThing.ResetState();
				DidInterceptMethodWithNoReturnValue = false;
				DidInterceptMethodWithReturnValue = false;

				Call.Static( () => StaticThing.SomeThing );
				const string realValue2 = "It's the real deal yo.";
				Call.StaticSetter( () => StaticThing.SomeThing, realValue2 );
				Call.Static( () => StaticThing.DoSomething() );
				Call.Static( () => StaticThing.DoSomethingThatReturns() );
				EnsureDoSomethingMethodWasNotCalled();
				EnsureDoSomethingThatReturnsMethodWasNotCalled();
				DidInterceptMethodWithNoReturnValue.ShouldBe( true );
				DidInterceptMethodWithReturnValue.ShouldBe( true );
			}


			[TestMethod]
			public virtual void AfterInterceptingThePropertyCall_WhenMakingSubsequentCalls_ShouldCallTheRealProperty() {
				InterceptTheNextCallToTheStaticProperties();

				// call the static properties, ensuring they get intercepted
				Call.Static( () => StaticThing.SomeThing );
				const string realValue1 = "It's the real deal yo.";
				Call.StaticSetter( () => StaticThing.SomeThing, realValue1 );
				EnsurePropertyGetterWasNotCalled();
//				EnsurePropertySetterWasNotCalled();

				StaticThing.ResetState();

				// call static methods again, ensuring the real methods were called
				Call.Static( () => StaticThing.SomeThing );
				const string realValue2 = "It's the real deal yo.";
				Call.StaticSetter( () => StaticThing.SomeThing, realValue2 );
				EnsurePropertyGetterWasCalled();
//				EnsurePropertySetterWasCalled();
			}


			[TestMethod]
			public virtual void AfterInterceptingTheMethodCall_WhenMakingSubsequentCalls_ShouldCallTheRealMethod() {
				InterceptTheNextCallToTheStaticMethods();

				// call the static methods, ensuring they get intercepted
				Call.Static( () => StaticThing.DoSomething() );
				Call.Static( () => StaticThing.DoSomethingThatReturns() );
				EnsureDoSomethingMethodWasNotCalled();
				EnsureDoSomethingThatReturnsMethodWasNotCalled();
				
				StaticThing.ResetState();

				// call static methods again, ensuring the real methods were called
				Call.Static( () => StaticThing.DoSomething() );
				Call.Static( () => StaticThing.DoSomethingThatReturns() );
				EnsureDoSomethingMethodWasCalled();
				EnsureDoSomethingThatReturnsMethodWasCalled();
			}

			#endregion


#if DEBUG

			#region sub-contexts

			[TestClass]
			public class OnceLockedDown : WhenConfiguredToInterceptAStaticCallAndReplaceItsBehaviour {

				#region context setup

				protected override void ContextSetup() {
					base.ContextSetup();

					UnitBoxInterceptions.Lockdown();
				}

				#endregion


				#region specs

				[TestMethod]
				public void WhenInvokingAStaticProperty_ShouldCallTheRealProperty() {
					WhenInvokingAStaticPropertyShouldCallTheRealProperty();
				}


				[TestMethod]
				public void WhenInvokingAStaticMethod_ShouldCallTheRealMethod() {
					WhenInvokingAStaticMethodShouldCallTheRealMethod();
				}

				#endregion


				#region non-specs

				[SpecNotApplicableInThisContext]
				public override void WhenMakingTheNextPropertyCall_ShouldInterceptThePropertyCall() {
				}
				[SpecNotApplicableInThisContext]
				public override void WhenMakingTheNextMethodCall_ShouldInterceptTheMethodCall() {
				}


				[SpecNotApplicableInThisContext]
				public override void WhenConfiguringASubsequentInterceptionToTheSameProperty_ShouldInterceptCallsInTheSameOrderTheyWereConfigured() {
				}
				[SpecNotApplicableInThisContext]
				public override void WhenConfiguringASubsequentInterceptionToTheSameMethod_ShouldInterceptCallsInTheSameOrderTheyWereConfigured() {
				}


				[SpecNotApplicableInThisContext]
				public override void AfterInterceptingThePropertyCall_WhenMakingSubsequentCalls_ShouldCallTheRealProperty() {
				}
				[SpecNotApplicableInThisContext]
				public override void AfterInterceptingTheMethodCall_WhenMakingSubsequentCalls_ShouldCallTheRealMethod() {
				}

				#endregion

			}

			#endregion

#endif


			#region helpers

			private static void InterceptTheNextCallToTheStaticProperties() {
				Intercept.NextCallTo( () => StaticThing.SomeThing )
					.AndInsteadDo( () => {
						DidInterceptPropertyGetter = true;
						return string.Empty;
					});
				// TODO(implement): intercept setter
				//				Intercept.NextCallTo( () => StaticThing.SomeThing )
				//					.AndInsteadDo( () => {
				//						DidInterceptPropertyGetter = true;
				//						return string.Empty;
				//					} );
			}


			private static void InterceptTheNextCallToTheStaticMethods() {
				Intercept.NextCallTo( () => StaticThing.DoSomething() )
					.AndInsteadDo( () => {
						DidInterceptMethodWithNoReturnValue = true;
					});
				Intercept.NextCallTo( () => StaticThing.DoSomethingThatReturns() )
					.AndInsteadDo( () => {
						DidInterceptMethodWithReturnValue = true;
						return string.Empty;
					});
			}

			#endregion

		}


		[TestClass]
		public class WhenConfiguredToInterceptAStaticCallAndReplaceItsBehaviour_VariousReplacements : InterceptSpecs {

			#region specs

			[TestMethod]
			public void ShouldBeAbleToReturnASpecificValue () {
				const string replacedReturnValue = "intercepted!";
				Intercept.NextCallTo( () => StaticThing.DoSomethingThatReturns() )
					.AndInsteadReturn( replacedReturnValue );

				string returnValue = Call.Static( () => StaticThing.DoSomethingThatReturns() );
				EnsureDoSomethingThatReturnsMethodWasNotCalled();
				returnValue.ShouldBe( replacedReturnValue );
			}


			[TestMethod]
			public void ShouldBeAbleToReplaceMethodInvocation() {
				const string interceptedReturnValue = "intercepted!";
				Intercept.NextCallTo( () => StaticThing.DoSomethingThatReturns() )
					.AndInsteadDo( () => interceptedReturnValue );
				// TODO(specs): add .AndInsteadDo(..) for an Action method

				string returnedValue = Call.Static( () => StaticThing.DoSomethingThatReturns() );
				EnsureDoSomethingThatReturnsMethodWasNotCalled();
				returnedValue.ShouldBe( interceptedReturnValue );
			}

			#endregion

		}

		#endregion


		#region helpers

		protected static void WhenInvokingAStaticPropertyShouldCallTheRealProperty() {
			// call the static property
			Call.Static( () => StaticThing.SomeThing );
			const string realValue = "It's the real deal yo.";
			Call.StaticSetter( () => StaticThing.SomeThing, realValue );

			// ensure the real property was called
			EnsurePropertyGetterWasCalled();
			// TODO(revive)
//			EnsurePropertySetterWasCalled();
		}


		protected static void WhenInvokingAStaticMethodShouldCallTheRealMethod() {
			// call the static method
			Call.Static( () => StaticThing.DoSomething() );
			Call.Static( () => StaticThing.DoSomethingThatReturns() );

			// ensure the real method was called
			EnsureDoSomethingMethodWasCalled();
			EnsureDoSomethingThatReturnsMethodWasCalled();
		}


		protected static void EnsurePropertyGetterWasCalled() {
			StaticThing.SomeThingGetterWasCalled.ShouldBe( true,
				"Real property getter should have been called but wasn't." );
		}


		protected static void EnsurePropertyGetterWasNotCalled() {
			StaticThing.SomeThingGetterWasCalled.ShouldBe( false,
				"Real property getter should have been intercepted but wasn't." );
		}


		protected static void EnsurePropertySetterWasCalled() {
			StaticThing.SomeThingSetterWasCalled.ShouldBe( true,
				"Real property setter should have been called but wasn't." );
		}


		protected static void EnsurePropertySetterWasNotCalled() {
			StaticThing.SomeThingSetterWasCalled.ShouldBe( false,
				"Real property setter should have been intercepted but wasn't." );
		}


		protected static void EnsureDoSomethingMethodWasCalled() {
			StaticThing.DoSomethingWasCalled.ShouldBe( true,
				"Real method should have been called but wasn't." );
		}


		protected static void EnsureDoSomethingMethodWasNotCalled() {
			StaticThing.DoSomethingWasCalled.ShouldBe( false,
				"Method should have been intercepted but wasn't." );
		}


		protected static void EnsureDoSomethingThatReturnsMethodWasCalled() {
			StaticThing.DoSomethingThatReturnsWasCalled.ShouldBe( true,
				"Real method should have been called but wasn't." );
		}


		protected static void EnsureDoSomethingThatReturnsMethodWasNotCalled() {
			StaticThing.DoSomethingThatReturnsWasCalled.ShouldBe( false,
				"Method should have been intercepted but wasn't." );
		}

		#endregion

	}
}
